/*
 * Implicit declarations:
 *
 *	struct bitmap_t {
 *	    uint16_t	width;
 *	    uint16_t	height;
 *	    string_t	data;
 *	};
 *
 *	struct pixmap_t {
 *	    uint16_t	width;
 *	    uint16_t	height;
 *	};
 *
 *	struct window_t {
 *	    uint16_t	width;
 *	    uint16_t	height;
 *	    int16_t	x;
 *	    int16_t	y;
 *	    uint32_t	pixel;
 *	    pixmap_t	pixmap;
 *	    uint8_t	popup;
 *	    uint8_t	input;
 *	    uint32_t	events;
 *	    window_t	parent;
 *	};
 *
 *	struct event_t {
 *	    window_t	window;
 *	    uint32_t	type;
 *	    uint16_t	width;
 *	    uint16_t	height;
 *	    int16_t	x;
 *	    int16_t	y;
 *	    uint32_t	state;
 *	    uint32_t	detail;
 *	};
 *
 *	struct context_t {
 *	    auto	drawable;
 *	    string_t	font;
 *	    uint32_t	background;
 *	    uint32_t	foreground;
 *	    uint16_t	line_width;
 *	};
 *
 *	struct timer {
 *	    window_t	window;
 *	    uint32_t	msec;
 *	    uint8_t	repeat;
 *	    uint8_t	pending;
 *	};
 *
 *	struct point {
 *	    int16_t	 x;
 *	    int16_t	 y;
 *	};
 *
 *	struct rect {
 *	    uint16_t	 width;
 *	    uint16_t	 height;
 *	    int16_t	 x;
 *	    int16_t	 y;
 *	};
 */

#define KeyPress		1
#define KeyRelease		2
#define ButtonPress		3
#define ButtonRelease		4
#define PointerMotion		5
#define ButtonMotion		6
#define Enter			7
#define Leave			8
#define FocusIn			9
#define FocusOut		10
#define Resize			11
#define Expose			12
#define MapNotify		13
#define UnmapNotify		14
#define Timer			15
#define Protocol		16
#  define DeleteWindowProtocol	0

#define ShiftMask		(1<<0)
#define LockMask		(1<<1)
#define ControlMask		(1<<2)
#define Mod1Mask		(1<<3)
#define Mod2Mask		(1<<4)
#define Mod3Mask		(1<<5)
#define Mod4Mask		(1<<6)
#define Mod5Mask		(1<<7)
#define Button1Mask		(1<<8)
#define Button2Mask		(1<<9)
#define Button3Mask		(1<<10)
#define Button4Mask		(1<<11)
#define Button5Mask		(1<<12)

#define Button1			1
#define Button2			2
#define Button3			3
#define Button4			4
#define Button5			5

#define KeyLeftTab		0xfe20		// XK_ISO_Left_Tab
#define KeyBackSpace		0xff08		// XK_BackSpace
#define KeyTab			0xff09		// XK_Tab
#define KeyReturn		0xff0d		// XK_Return
#define KeyEscape		0xff1b		// XK_Escape
/* FIXME handle XK_KP_<key> */
#define KeyHome			0xff50		// XK_Home
#define KeyLeft			0xff51		// XK_Left
#define KeyUp			0xff52		// XK_Up
#define KeyRight		0xff53		// XK_Right
#define KeyDown			0xff54		// XK_Down
#define KeyPageUp		0xff55		// XK_Page_Up
#define KeyPageDown		0xff56		// XK_Page_Down
#define KeyEnd			0xff57		// XK_End
#define KeyInsert		0xff63		// XK_Insert
#define KeyDelete		0xffff		// XK_Delete

#define normal_color		0xe0e0e0
#define pressed_color		0xd7d7d7
#define light_color		0xffffff
#define shadow_color		0x777777
#define darker_color		0x555555
#define text_back_color		0xf8f8f8

#define DefaultFont		"sans:pixelsize=12"

#define TypeMismatchException		5
#define InvalidArgumentException	7

open_display();

auto exit_flag = false;

void set_exit_flag() {
    exit_flag = true;
}

auto get_exit_flag() {
    return exit_flag;
}

context_t normal_gc = {
    foreground:		normal_color,
    font:		DefaultFont
};
create_context(normal_gc);

context_t dark_gc = {
    foreground:		darker_color,
    background:		normal_color,
    font:		DefaultFont
};
create_context(dark_gc);

context_t shadow_gc = {
    foreground:		shadow_color,
    background:		normal_color,
    font:		DefaultFont
};
create_context(shadow_gc);

context_t light_gc = {
    foreground:		light_color,
    font:		DefaultFont
};
create_context(light_gc);

context_t text_gc = {
    foreground:		0x000000,
    font:		DefaultFont
};
create_context(text_gc);

context_t text_back_gc = {
    foreground:		text_back_color,
    font:		DefaultFont
};
create_context(text_back_gc);

context_t focus_gc = {
    foreground:		0x666666,
    font:		DefaultFont
};
create_context(focus_gc);

context_t menu_focus_gc = {
    foreground:		0x2288bb,
    font:		DefaultFont
};
create_context(menu_focus_gc);

context_t sensitive_gc = {
    foreground:		0xaaaaaa,
    background:		normal_color,
    font:		DefaultFont
};
create_context(sensitive_gc);

context_t select_gc = {
    foreground:		0xeeeeee,
    font:		DefaultFont
};
create_context(select_gc);

bitmap_t arrow_up = {
    width:		10,
    height:		6,
    data:		{
	0b00110000, 0b00,
	0b01111000, 0b00,
	0b11111100, 0b00,
	0b11001110, 0b01,
	0b10000111, 0b11,
	0b00000011, 0b11,
    }
};
create_bitmap(arrow_up);

bitmap_t arrow_down = {
    width:		10,
    height:		6,
    data:		{
	0b00000011, 0b11,
	0b10000111, 0b11,
	0b11001110, 0b01,
	0b11111100, 0b00,
	0b01111000, 0b00,
	0b00110000, 0b00,
    }
};
create_bitmap(arrow_down);

bitmap_t arrow_left = {
    width:		6,
    height:		10,
    data:		{
	0b110000,
	0b111000,
	0b011100,
	0b001110,
	0b000111,
	0b000111,
	0b001110,
	0b011100,
	0b111000,
	0b110000,
    }
};
create_bitmap(arrow_left);

bitmap_t arrow_right = {
    width:		6,
    height:		10,
    data:		{
	0b000011,
	0b000111,
	0b001110,
	0b011100,
	0b111000,
	0b111000,
	0b011100,
	0b001110,
	0b000111,
	0b000011,
    }
};
create_bitmap(arrow_right);

hash_t dispatch = hash();

void paint_relief(window_t window, int32_t x0, int32_t y0,
		  int32_t x1, int32_t y1, int32_t raised) {
    shadow_gc.drawable = window;
    change_context(shadow_gc);
    dark_gc.drawable = window;
    change_context(dark_gc);
    light_gc.drawable = window;
    change_context(light_gc);
    if (raised) {
	draw_line(dark_gc,	x0 + 1, y1 - 1, x1 - 2, y1 - 1);
	draw_line(dark_gc,	x1 - 1, y1 - 2, x1 - 1, y0 + 1);
	draw_line(shadow_gc,	x1 - 2, y0,     x0 + 1, y0);
	draw_line(shadow_gc,	x0,     y0 + 1, x0,     y1 - 2);
	draw_line(light_gc,	x1 - 3, y0 + 1, x0 + 2, y0 + 1);
	draw_line(light_gc,	x0 + 1, y0 + 2, x0 + 1, y1 - 3);
    }
    else {
	draw_line(shadow_gc,	x0 + 1, y1 - 1, x1 - 2, y1 - 1);
	draw_line(shadow_gc,	x1 - 1, y1 - 2, x1 - 1, y0 + 1);
	draw_line(dark_gc,	x1 - 2, y0,     x0 + 1, y0);
	draw_line(dark_gc,	x0,     y0 + 1, x0,     y1 - 2);
	draw_line(shadow_gc,	x1 - 5, y0 + 3, x0 + 4, y0 + 3);
	draw_line(shadow_gc,	x0 + 3, y0 + 4, x0 + 3, y1 - 5);
	draw_line(light_gc,	x0 + 5, y1 - 3, x1 - 4, y1 - 3);
	draw_line(light_gc,	x1 - 3, y1 - 4, x1 - 3, y0 + 5);
    }
}

class gadget_t {
    uint16_t	width;
    uint16_t	height;
    int16_t	x;
    int16_t	y;
    window_t	window;		// parent widget window
    void paint() {
    }
};

class widget_t;
widget_t focus_widget, grab_widget;
void set_focus_widget(widget_t focus);
widget_t get_focus_widget();
void set_grab_widget(widget_t grab);
widget_t get_grab_widget();

class composite_t, popup_t;

class widget_t {
    widget_t	prev;
    widget_t	next;
    composite_t	parent;
    window_t	window;
    uint8_t	inside;
    uint8_t	sensitive;
    uint8_t	tabstop;
    uint32_t	state;
    widget_t();
    void setup() {
	if (window == null)
	    window = new window_t {
		width:		1,
		height:		1,
		pixel:		normal_color,
	    };
    }
    void realize() {
	/* in case properties changed */
	change_window(window);
	map_window(window);
    }
    void focus_in() {
	set_focus_widget(this);
    }
    void focus_out() {
    }
    void paint() {
    }
    void button_press(event_t event) {
	set_focus_widget(this);
	if (event.detail >= Button1 && event.detail <= Button5)
	    state |= Button1Mask <<< (event.detail - 1);
    }
    void button_release(event_t event) {
	if (event.detail >= Button1 && event.detail <= Button5)
	    state &= ~(Button1Mask <<< (event.detail - 1));
    }
    void button_motion(event_t event) {
    }
    void pointer_motion(event_t event) {
    }
    void key_press(event_t event, int32_t keysym, string_t buffer) {
    }
    void key_release(event_t event) {
    }
    void resize() {
    }
    void enter(event_t event) {
	inside = true;
    }
    void leave(event_t event) {
	inside = false;
    }
    void map() {
    }
    void unmap() {
    }
    void timer(timer_t time) {
    }
    auto get_sensitive();
    void set_sensitive(state) {
	sensitive = !!state;
    }
    widget_t tab_head() {
	if (sensitive && tabstop)
	    return this;
	return null;
    }
    widget_t tab_tail() {
	if (sensitive && tabstop)
	    return this;
	return null;
    }
    widget_t tab_next();
    widget_t tab_prev();
};

void set_focus_widget(widget_t focus) {
    if (focus_widget != focus) {
	widget_t old_focus = focus_widget;
	if ((focus_widget = focus))
	    focus_widget.focus_in();
	if (old_focus)
	    old_focus.focus_out();
    }
}

widget_t get_focus_widget() {
    return focus_widget;
}

void set_grab_widget(widget_t grab) {
    if (grab_widget != grab) {
	if ((grab_widget = grab))
	    grab_input(grab.window);
	else
	    ungrab_input();
    }
}

widget_t get_grab_widget() {
    return grab_widget;
}

class composite_t : widget_t {
    widget_t	children;
    composite_t() {
	widget_t();
    }
    void realize() {
	widget_t	child = children;
	if (child) {
	    do {
		child.realize();
		child = child.next;
	    } while (child != children);
	}
	widget_t.realize();
    }
    void button_press(event_t event) {
	/* don't set focus */
    }
    void button_release(event_t event) {
    }
    void manage(widget_t widget) {
	widget.parent = this;
	if (children == null)
	    children = widget.prev = widget.next = widget;
	else {
	    widget.prev = children.prev;
	    children.prev.next = widget;
	    children.prev = widget;
	    widget.next = children;
	}
	widget.set_sensitive(sensitive);
    }
    void unmanage(widget_t widget) {
	if (widget.parent == this) {
	    if (children == widget) {
		if (widget.next == widget)
		    children = null;
		else {
		    widget.next.prev = widget.prev;
		    widget.prev.next = widget.next;
		}
	    }
	    widget.parent = null;
	}
	else
	    throw InvalidArgumentException;
    }
    widget_t tab_head() {
	widget_t head;
	widget_t focus = widget_t.tab_head();
	if (focus)
	    return focus;
	if ((head = children)) {
	    do {
		if ((focus = head.tab_head()))
		    return focus;
		head = head.next;
	    } while (head != children);
	}
	return null;
    }
    widget_t tab_tail() {
	widget_t left;
	widget_t tail;
	widget_t focus = widget_t.tab_tail();
	if (focus)
	    return focus;
	if ((tail = children)) {
	    left = tail = tail.prev;
	    do {
		if ((focus = tail.tab_tail()))
		    return focus;
		tail = tail.prev;
	    } while (tail != left);
	}
	return null;
    }
    widget_t tab_next() {
	widget_t focus = widget_t.tab_next();
	if (focus == null && (focus = children)) {
	    if (focus.tab_head() == focus) {
		set_focus_widget(focus);
		return focus;
	    }
	    focus = focus.tab_next();
	}
	return focus;
    }
    widget_t tab_prev() {
	widget_t focus = widget_t.tab_prev();
	if (focus == null && children)
	    focus = children.tab_prev();
	return focus;
    }
};

widget_t.widget_t() {
    setup();
    window.events |= (1 << Resize)	|
		     (1 << ButtonPress);
    if (parent) {
	if (subtypeof(parent, composite_t)) {
	    window.parent = parent.window;
	    parent.manage(this);
	}
	else if (!subtypeof(this, popup_t))
	    throw InvalidArgumentException;
	else
	    set_sensitive(true);
    }
    else
	set_sensitive(true);
    create_window(window);
    dispatch[window] = this;
}
widget_t widget_t.tab_next() {
    widget_t head;
    widget_t focus;
    for (focus = next; focus && focus != this; focus = focus.next) {
	if ((head = focus.tab_head())) {
	    set_focus_widget(head);
	    return head;
	}
    }
    if (parent)
	return parent.tab_next();
    return null;
}
widget_t widget_t.tab_prev() {
    widget_t tail;
    widget_t focus;
    for (focus = prev; focus && focus != this; focus = focus.prev) {
	if ((tail = focus.tab_tail())) {
	    set_focus_widget(tail);
	    return tail;
	}
    }
    if (parent)
	return parent.tab_prev();
    return null;
}

auto widget_t.get_sensitive() {
    if (sensitive)
	return !parent || parent.sensitive;
    return false;
}

class menubar_t;
class viewport_t;

class toplevel_t : composite_t {
    widget_t	menubar;
    widget_t	viewport;
    toplevel_t() {
	composite_t();
    }
    void setup() {
	widget_t.setup();
	window.events = (1 << KeyPress)	|
			(1 << FocusIn)	|
			(1 << FocusOut)	|
			(1 << Protocol);
    }
    void resize() {
	int32_t	y, width, height;
	if ((width = window.width) < 20)
	    width = 20;
	if (menubar) {
	    menubar.window.width = width;
	    change_window(menubar.window);
	    y = menubar.window.height;
	    height = window.height - menubar.window.height;
	}
	else {
	    y = 0;
	    height = window.height;
	}
	if (viewport) {
	    if (height < 20)
		height = 20;
	    viewport.window.y = y;
	    viewport.window.width = width;
	    viewport.window.height = height;
	    change_window(viewport.window);
	}
    }
    void key_press(event_t event, int32_t keysym, string_t buffer) {
	widget_t focus = get_focus_widget();
	switch (keysym) {
	    case KeyTab:
		if (focus)
		    focus.tab_next();
		break;
	    case KeyLeftTab:
		if (focus)
		    focus.tab_prev();
		break;
	    default:
		if (focus && focus != this)
		    focus.key_press(event, keysym, buffer);
		break;
	}
    }
    void manage(widget_t widget) {
	composite_t.manage(widget);
	if (widget.parent == this) {
	    if (subtypeof(widget, menubar_t))
		menubar = widget;
	    else if (subtypeof(widget, viewport_t))
		viewport = widget;
	}
    }
    void unmanage(widget_t widget) {
	composite_t.unmanage(widget);
	if (widget.parent != this) {
	    int32_t change = false;
	    if (menubar == widget)
		change = true, menubar = null;
	    else if (viewport == widget)
		change = true, viewport = null;
	    if (change)
		resize();
	}
    }
    void event_loop() {
	string_t    buffer = new uint8_t[32];
	int32_t	    keysym;
	timer_t     timer;
	event_t     event = {};
	widget_t    grab, temp;
	point_t     point = {};
	widget_t    widget;
	while (!get_exit_flag()) {
	    next_event(event);
	    if ((widget = dispatch[event.window]) == null) {
		print("oops [%a]=null\n", event.window);
		continue;
	    }
	    switch (event.type) {
		case KeyPress:
		    keysym = trans_text(window, event, buffer);
		    if (widget.get_sensitive())
			widget.key_press(event, keysym, buffer);
		    break;
		case KeyRelease:
		    if (widget.get_sensitive())
			widget.key_release(event);
		    break;
		case ButtonPress:
		    if (widget.get_sensitive()) {
			/* if there is a grab, only forward the event
			 * if it is for the grab widget or a children,
			 * otherwise, translate the event and send to
			 * the grab widget; this is required because,
			 * when releasing the grab, the widget that
			 * receives the button press doesn't have an
			 * "implicit" grab */
			if ((grab = get_grab_widget())) {
			    temp = widget;
			    while (temp && temp != grab)
				temp = temp.parent;
			    if (!temp) {
				widget = get_grab_widget();
				trans_coord(event.window, widget.window,
					    event.x, event.y, point);
				event.x = point.x;
				event.y = point.y;
				event.window = widget.window;
			    }
			}
			widget.button_press(event);
		    }
		    break;
		case ButtonRelease:
		    if (widget.get_sensitive())
			widget.button_release(event);
		    break;
		case ButtonMotion:
		    if (widget.get_sensitive())
			widget.button_motion(event);
		    break;
		case PointerMotion:
		    if (widget.get_sensitive())
			widget.pointer_motion(event);
		    break;
		case Enter:
		    if (widget.get_sensitive())
			widget.enter(event);
		    break;
		case Leave:
		    if (widget.get_sensitive())
			widget.leave(event);
		    break;
		case Expose:
		    widget.paint();
		    break;
		case FocusIn:
		    /* only toplevel should have focus */
		    if (get_focus_widget() == null)
			set_focus_widget(this);
		    break;
		case FocusOut:
		    /* only toplevel should have focus */
		    if (!get_grab_widget())
			set_focus_widget(null);
		    break;
		case Resize:
		    widget.resize();
		    break;
		case MapNotify:
		    widget.map();
		    break;
		case UnmapNotify:
		    widget.unmap();
		    break;
		case Timer:
		    if (widget.get_sensitive())
			widget.timer(event.window);
		    break;
		case Protocol:
		    if (event.state == DeleteWindowProtocol)
			set_exit_flag();
		    break;
		default:
		    print("unhandled event %d\n", event.type);
		    break;
	    }
	}
    }
};

struct textline_t : rect_t {
    uint32_t	offset;
    uint32_t	length;
};

class label_t : widget_t {
    rect_t	rect;		// scratch rect
    textline_t	table[];	// line table
    string_t	string;		// string buffer
    label_t() {
	if (string == null)
	    string = {
	    };
	rect = text_rect(text_gc, "");
	build_table();
	widget_t();
    }
    void setup() {
	textline_t	temp;
	int32_t		width, height, i, size = sizeof(table);
	widget_t.setup();
	for (i = 0; i < size; i++) {
	    temp = table[i];
	    if (width < temp.width + 10)
		width = temp.width + 10;
	}
	height = (temp.y + temp.height) - table[0].y + 10;
	if (window.width <= 10)
	    window.width = width;
	if (window.height <= table[0].height)
	    window.height = height;
    }
    void paint() {
	textline_t	temp;
	int32_t		i, count = sizeof(table);
	context_t	gc = get_sensitive() ? text_gc : sensitive_gc;
	/* clear entire window */
	clear_rect(window, 0, 0, window.width, window.height, false);
	gc.drawable = window;
	change_context(gc);
	for (i = 0; i < count; i++) {
	    temp = table[i];
	    if (temp.y + temp.height <= 0)
		continue;
	    if (temp.y >= window.height)
		break;
	    draw_text(gc, temp.x, temp.y, string, temp.offset, temp.length);
	}
    }
    /* TODO should only build a "line table" of the visible text,
     * and should also support wrapping */
    void build_table() {
	int32_t	size = sizeof(string);
	int32_t	i, j, y, length, position;
	for (i = j = 0, y = 5, position = 0; i < size; i++) {
	    if (string[i] == '\n') {
		length = i - position;
		text_rect(text_gc, string, rect, position, length);
		add_line(j, 5, y, rect.width, rect.height, position, length);
		y += rect.height + 1;
		position = i + 1;
		++j;
	    }
	}
	length = size - position;
	text_rect(text_gc, string, rect, position, length);
	add_line(j, 5, y, rect.width, rect.height, position, length);
	if (++j != sizeof(table))
	    renew(table, j);
    }
    void add_line(int32_t i, int32_t x, int32_t y,
		  int32_t width, int32_t height,
		  int32_t offset, int32_t length) {
	textline_t	temp;
	int32_t		size = sizeof(table);
	if (i < size) {
	    temp = table[i];
	    temp.x = x;
	    temp.y = y;
	    temp.width = width;
	    temp.height = height;
	    temp.offset = offset;
	    temp.length = length;
	}
	else {
	    if (size == 0)
		table = new textline_t[i + 1];
	    else if (i >= size)
		renew(table, i + 1);
	    temp = table[i] = {
		x:		x,
		y:		y,
		width:		width,
		height:		height,
		offset:		offset,
		length:		length
	    };
	}
    }
};

class button_t : label_t {
    button_t() {
	label_t();
	resize();
	tabstop = true;
    }
    void setup() {
	label_t.setup();
	window.events = (1 << ButtonRelease)	|
			(1 << Expose) 	 	|
			(1 << Enter)		|
			(1 << Leave);
    }
    void resize() {
	textline_t	temp;
	int32_t		i, size = sizeof(table), x, y, width, height;
	for (i = 0; i < size; i++) {
	    temp = table[i];
	    if (width < temp.width)
		width = temp.width;
	}
	height = (temp.y + temp.height) - table[0].y;
	x = (window.width - width) \ 2;
	y = (window.height - height) \ 2;
	/* if button pressed */
	if (state & Button1Mask)
	    ++x, ++y;
	for (i = 0; i < size; i++) {
	    temp = table[i];
	    /* should default (and be configurable?) to center */
	    temp.x = x;
	    temp.y = y;
	    y += temp.height + 1;
	}
    }
    void focus_in() {
	set_focus_widget(this);
	clear_rect(window, 0, 0, window.width, window.height, true);
    }
    void focus_out() {
	clear_rect(window, 0, 0, window.width, window.height, true);
    }
    void paint() {
	int32_t pressed = state & Button1Mask;
	int32_t focus = get_focus_widget() == this;
	label_t.paint();
	paint_relief(window, 0, 0, window.width, window.height, !pressed);
	if (!pressed && focus) {
	    focus_gc.drawable = window;
	    change_context(focus_gc);
	    draw_rect(focus_gc, 3, 3, window.width - 7, window.height - 7);
	}
    }
    void button_press(event_t event) {
	int32_t pressed = state & Button1Mask;
	widget_t.button_press(event);
	if (!pressed && (state & Button1Mask)) {
	    textline_t	temp;
	    int32_t	i, size = sizeof(table);
	    for (i = 0; i < size; i++) {
		temp = table[i];
		++temp.x;
		++temp.y;
	    }
	    window.pixel = pressed_color;
	    change_window(window);
	}
    }
    void button_release(event_t event) {
	int32_t pressed = state & Button1Mask;
	widget_t.button_release(event);
	if (pressed && !(state & Button1Mask)) {
	    textline_t	temp;
	    int32_t	i, size = sizeof(table);
	    for (i = 0; i < size; i++) {
		temp = table[i];
		--temp.x;
		--temp.y;
	    }
	    window.pixel = normal_color;
	    change_window(window);
	    if (inside)
		print("button '%s' pressed\n", string);
	}
    }
};

#define	Up		0
#define Down		1
#define Left		2
#define Right		3

#define Vertical	0
#define Horizontal	1

struct scroll_t {
    float64_t	line;
    float64_t	page;
    float64_t	start;
    float64_t	range;
};

class scrollbar_t;

class clipview_t : composite_t {
    void get_scroll(scroll_t scroll, int32_t orientation) {
	scroll.start = 0;
	scroll.line = scroll.page = scroll.range = 1;
    }
    void set_scroll(scroll_t scroll, int32_t orientation) {
    }
};

class clipwidget_t : clipview_t {
    composite_t	widget;
    clipwidget_t() {
	clipview_t();
	widget = {
	    parent:		this,
	    window: {
		width:		256,
		height:		256,
		pixel:		normal_color
	    }
	};
    }
    void setup() {
	widget_t.setup();
	window.pixel = shadow_color;
    }
    void get_scroll(scroll_t scroll, int32_t orientation) {
	if (orientation == Vertical) {
	    scroll.range = widget.window.height;
	    scroll.start = -widget.window.y;
	    scroll.page = window.height;
	    scroll.line = 16;
	    if (scroll.start + scroll.page > scroll.range) {
		if ((scroll.start = scroll.range - scroll.page) < 0)
		    scroll.start = 0;
		set_scroll(scroll, orientation);
	    }
	}
	else {
	    scroll.range = widget.window.width;
	    scroll.start = -widget.window.x;
	    scroll.page = window.width;
	    scroll.line = 16;
	    if (scroll.start + scroll.page > scroll.range) {
		if ((scroll.start = scroll.range - scroll.page) < 0)
		    scroll.start = 0;
		set_scroll(scroll, orientation);
	    }
	}
    }
    void set_scroll(scroll_t scroll, int32_t orientation) {
	if (orientation == Vertical)
	    widget.window.y = -scroll.start;
	else
	    widget.window.x = -scroll.start;
	change_window(widget.window);
    }
};

/* scrollbar repeaters */
class repeater_t : widget_t {
    timer_t	time;
    uint8_t	direction;
    bitmap_t	bitmap;
    repeater_t(void) {
	switch (direction) {
	    case Up:
		bitmap = arrow_up;
		break;
	    case Down:
		bitmap = arrow_down;
		break;
	    case Left:
		bitmap = arrow_left;
		break;
	    default:
		bitmap = arrow_right;
		break;
	}
	time = new timer_t { window: this, msec: 150, repeat: true };
	widget_t();
    }
    void setup() {
	widget_t.setup();
	window.events = (1 << ButtonRelease)	|
			(1 << Expose) 	 	|
			(1 << Enter)		|
			(1 << Leave);
    }
    void paint() {
	context_t gc;
	int32_t x, y;
	int32_t pressed = state & Button1Mask;
	paint_relief(window, 0, 0, window.width, window.height, !pressed);
	x = direction == Up || direction == Down ? 5 : 6;
	y = direction == Up || direction == Down ? 6 : 5;
	if (get_sensitive()) {
	    if (pressed) {
		++x;
		++y;
	    }
	    gc = dark_gc;
	}
	else {
	    gc = sensitive_gc;
	    gc.drawable = window;
	    change_context(gc);
	}
	copy_rect(gc, x, y, bitmap, 0, 0, bitmap.width, bitmap.height);
    }
    void button_press(event_t event) {
	widget_t.button_press(event);
	if (state & Button1Mask) {
	    if (inside) {
		window.pixel = pressed_color;
		change_window(window);
		/* FIXME should directly call method paint()
		 * but some artifacts may remain after XCopyPlane()
		 */
		clear_rect(window, 0, 0, window.width, window.height, true);
		arm_timer();
	    }
	}
    }
    void button_release(event_t event) {
	int32_t pressed = state & Button1Mask;
	widget_t.button_release(event);
	if (pressed && !(state & Button1Mask)) {
	    window.pixel = normal_color;
	    change_window(window);
	    clear_rect(window, 0, 0, window.width, window.height, true);
	    disarm_timer();
	}
    }
    void timer(timer_t time);
    void set_sensitive(value) {
	if (sensitive != value) {
	    if (!(sensitive = value)) {
		state = 0;
		if (time.pending)
		    disarm_timer();
	    }
	    clear_rect(window, 0, 0, window.width, window.height, true);
	}
    }
    void arm_timer() {
	create_timer(time);
	dispatch[time] = this;
	/* call callbacks once before triggering timer */
	timer(time);
    }
    void disarm_timer() {
	destroy_timer(time);
	dispatch[time] = null;
    }
};

class thumb_t : gadget_t {
    void paint() {
	normal_gc.drawable = window;
	change_context(normal_gc);
	fill_rect(normal_gc, x + 2, y + 2, width - 3, height - 3);
	paint_relief(window, x, y, x + width, y + height, true);
    }
};

class scrollbar_t : composite_t {
    uint8_t	orientation;
    uint8_t	direction;
    uint8_t	thumbing;
    uint16_t	dimension;
    repeater_t	less;
    repeater_t	more;
    thumb_t	thumb;
    clipview_t	clip;
    timer_t	time;
    uint32_t	offset;
    scroll_t	scroll;
    scrollbar_t() {
	int32_t ldir, mdir, size;
	int32_t rx, ry, rwidth, rheight;
	if (orientation == Vertical) {
	    rwidth = 20;
	    rheight = 18;
	    rx = 0;
	    ldir = Up;
	    mdir = Down;
	}
	else {
	    rwidth = 18;
	    rheight = 20;
	    ry = 0;
	    ldir = Left;
	    mdir = Right;
	}
	composite_t();
	dimension = orientation == Vertical ? window.height : window.width;
	if (orientation == Vertical)
	    ry = window.height - 18;
	else
	    rx = window.width - 18;

	less = new repeater_t {
	    direction:		ldir,
	    parent:		this,
	    window: {
		width:		rwidth,
		height:		rheight,
		pixel:		normal_color,
	    }
	};
	more = new repeater_t {
	    direction:		mdir,
	    parent:		this,
	    window: {
		x:		rx,
		y:		ry,
		width:		rwidth,
		height:		rheight,
		pixel:		normal_color,
	    }
	};
	if ((dimension -= 36) < 18)
	    dimension = 18;

	if (scroll == null)
	    scroll = new scroll_t;
	if (clip)
	    clip.get_scroll(scroll, orientation);

	size = trunc(scroll.page * (dimension / scroll.range));
	if (size < 18)
	    size = 18;
	if (orientation == Vertical) {
	    thumb = new thumb_t {
		x:		0,
		y:		18,
		width:		20,
		height:		size,
		window:		window,
	    };
	}
	else {
	    thumb = new thumb_t {
		x:		18,
		y:		0,
		width:		size,
		height:		20,
		window:		window,
	    };
	}
	time = {
	    window:		this,
	    msec:		150,
	    repeat:		true
	};
    }
    void setup() {
	widget_t.setup();
	window.pixel = 0xefefef;
	if (orientation == Vertical) {
	    if (window.width < 20)
		window.width = 20;
	    if (window.height < 54)
		window.height = 54;
	}
	else {
	    if (window.width < 54)
		window.width = 54;
	    if (window.height < 20)
		window.height = 20;
	}
	window.events = (1 << ButtonMotion)	|
			(1 << ButtonRelease)	|
			(1 << Expose) 	 	|
			(1 << Enter)		|
			(1 << Leave);
    }
    void resize() {
	/* FIXME it should be the clip widget to tell about scroll
	 * data change, because the scrollbar may be resized first */
	if (clip)
	    clip.get_scroll(scroll, orientation);
	if (orientation == Vertical) {
	    dimension = window.height;
	    more.window.y = dimension - 18;
	}
	else {
	    dimension = window.width;
	    more.window.x = dimension - 18;
	}
	change_window(more.window);
	if ((dimension -= 36) < 18)
	    dimension = 18;
	update_thumb();
	/* only adjust_thumb() calls update_sensitive() */
	update_sensitive();
    }
    void paint() {
	shadow_gc.drawable = window;
	change_context(shadow_gc);
	light_gc.drawable = window;
	change_context(light_gc);
	if (orientation == Vertical) {
	    draw_line(shadow_gc, 0, 18, 0, window.height - 19);
	    draw_line(light_gc, 19, 18, 19, window.height - 19);
	}
	else {
	    draw_line(shadow_gc, 18, 0, window.width - 19, 0);
	    draw_line(light_gc, 18, 19, window.width - 19, 19);
	}
	thumb.paint();
    }
    void button_press(event_t event) {
	widget_t.button_press(event);
	if (state & Button1Mask) {
	    if (orientation == Vertical) {
		offset = event.y;
		if (event.y >= thumb.y && event.y <  thumb.y + thumb.height) {
		    thumbing = true;
		    offset -= thumb.y;
		}
		else
		    direction = offset < thumb.y ? Up : Down;
	    }
	    else {
		offset = event.x;
		if (event.x >= thumb.x && event.x <  thumb.x + thumb.width) {
		    thumbing = true;
		    offset -= thumb.x;
		}
		else
		    direction = offset < thumb.x ? Up : Down;
	    }
	    if (!thumbing) {
		create_timer(time);
		dispatch[time] = this;
		/* call callback once before triggering timer */
		timer(time);
	    }
	}
    }
    void button_motion(event_t event) {
	int32_t pos, check;
	if (inside) {
	    pos = orientation == Vertical ? event.y : event.x;
	    if (thumbing) {
		pos -= offset;
		check = (pos - 18) * (scroll.range / dimension);
		adjust_thumb(check);
	    }
	    else
		/* update paging timer information */
		offset = pos;
	}
    }
    void button_release(event_t event) {
	int32_t pressed = state & Button1Mask;
	widget_t.button_release(event);
	if (pressed && !(state & Button1Mask)) {
	    if (thumbing)
		thumbing = false;
	    else if (time.pending) {
		destroy_timer(time);
		dispatch[time] = null;
	    }
	}
    }
    void timer(timer_t time) {
	if (inside) {
	    int32_t check;
	    int32_t pos = orientation == Vertical ? thumb.y : thumb.x;
	    if (direction == Up) {
		if (pos < offset)
		    /* if top of thumb is above pointer coordinate */
		    return;
		check = scroll.start - scroll.page;
	    }
	    else {
		pos += orientation == Vertical ? thumb.height : thumb.width;
		if (pos > offset)
		    /* if bottom of thumb is below pointer coordinate */
		    return;
		check = scroll.start + scroll.page;
	    }
	    adjust_thumb(check);
	}
    }
    void adjust_thumb(check) {
	if (check > scroll.range - scroll.page)
	    check = scroll.range - scroll.page;
	if (check < 0)
	    check = 0;
	if (check != scroll.start) {
	    scroll.start = check;
	    update_thumb();
	}
	else if (time.pending) {
	    destroy_timer(time);
	    dispatch[time] = null;
	}
	update_sensitive();
    }
    void inc_line() {
	adjust_thumb(scroll.start + scroll.line);
    }
    void dec_line() {
	adjust_thumb(scroll.start - scroll.line);
    }
    void update_thumb() {
	auto old_pos, old_dim;
	auto new_pos, new_dim;
	if (orientation == Vertical) {
	    old_pos = thumb.y;
	    old_dim = thumb.height;
	}
	else {
	    old_pos = thumb.x;
	    old_dim = thumb.width;
	}
	new_pos = trunc(scroll.start * (dimension / scroll.range));
	if (new_pos > dimension - 18)
	    new_pos = dimension - 18;
	if (new_pos < 0)
	    new_pos = 0;
	new_pos += 18;
	new_dim = trunc(scroll.page * (dimension / scroll.range));
	if (new_dim < 18)
	    new_dim = 18;
	/* adjust rounding */
	if (scroll.start >= scroll.range - scroll.page &&
	    (new_pos - 18) + new_dim != dimension)
	    new_dim = dimension - (new_pos - 18);
	if (old_pos != new_pos || old_dim != new_dim) {
	    if (orientation == Vertical) {
		clear_rect(window, 0, old_pos, 20, old_dim, false);
		thumb.y = new_pos;
		thumb.height = new_dim;
	    }
	    else {
		clear_rect(window, old_pos, 0, old_dim, 20, false);
		thumb.x = new_pos;
		thumb.width = new_dim;
	    }
	    paint();
	}
	if (clip)
	    clip.set_scroll(scroll, orientation);
    }
    void update_sensitive() {
	less.set_sensitive(scroll.start > 0);
	more.set_sensitive(scroll.start + scroll.page < scroll.range);
    }
};

void repeater_t.timer(timer_t time) {
    if (inside) {
	scrollbar_t scrollbar = parent;
	if (direction == Up || direction == Left) {
	    scrollbar.dec_line();
	}
	else
	    scrollbar.inc_line();
    }
}

class viewport_t : composite_t {
    scrollbar_t	vscroll;
    scrollbar_t	hscroll;
    clipview_t	clipview;
    void resize() {
	int32_t	width		= window.width;
	int32_t	height		= window.height;
	int32_t	min_width	= 20;
	int32_t	min_height	= 20;
	if (hscroll) {
	    min_width += 34;
	    min_height += 20;
	}
	if (vscroll) {
	    min_width += 20;
	    min_height += 34;
	}
	if (width < min_width)
	    width = min_width;
	if (height < min_height)
	    height = min_height;
	if (hscroll)
	    width -= 20;
	if (vscroll)
	    height -= 20;
	if (hscroll) {
	    hscroll.window.y = height;
	    hscroll.window.width = width;
	    change_window(hscroll.window);
	}
	if (vscroll) {
	    vscroll.window.x = width;
	    vscroll.window.height = height;
	    change_window(vscroll.window);
	}
	if (clipview) {
	    clipview.window.width = width;
	    clipview.window.height = height;
	    change_window(clipview.window);
	}
    }
    void manage(widget_t widget) {
	composite_t.manage(widget);
	if (widget.parent == this) {
	    if (subtypeof(widget, scrollbar_t)) {
		scrollbar_t	scroll = widget;
		if (scroll.orientation == Vertical)
		    vscroll = widget;
		else
		    hscroll = widget;
	    }
	    else if (subtypeof(widget, clipview_t))
		clipview = widget;
	}
    }
    void unmanage(widget_t widget) {
	composite_t.unmanage(widget);
	if (widget.parent != this) {
	    auto change = false;
	    if (vscroll == widget)
		change = true, vscroll = null;
	    else if (hscroll == widget)
		change = true, hscroll = null;
	    else if (clipview == widget)
		change = true, clipview = null;
	    if (change)
		resize();
	}
    }
};

class menuitem_t : gadget_t {
    rect_t	rect;
    string_t	label;
    menuitem_t	next;
    menuitem_t	prev;
    widget_t	popup;
    menuitem_t() {
	rect = text_rect(text_gc, label);
	x = 8;
	y = 3;
	width = rect.width + 16;
	height = rect.height + 6;
    }
    void draw(selected) {
	if (selected) {
	    fill_rect(menu_focus_gc, x + 1, y + 1, width - 2, height - 2);
	    draw_line(shadow_gc, x + 1, y + height - 1,
		      x + width - 2, y + height - 1);
	    draw_line(shadow_gc, x + width - 1, y + height - 2,
		      x + width - 1, y + 1);
	    draw_line(dark_gc, x + 1, y,
		      x + width - 2, y);
	    draw_line(dark_gc, x, y + 1,
		      x, y + height - 2);
	    draw_text(select_gc, x + 8, y + 2, label);
	}
	else
	    draw_text(text_gc, x + 8, y + 2, label);
    }
};

#define MenuDecorationWidth	4
#define MenuDecorationHeight	4
class menubar_t : widget_t {
    menuitem_t	first;
    menuitem_t	entry;
    point_t	point;
    int16_t	offset;
    menubar_t() {
	offset = MenuDecorationWidth;
	widget_t();
	/* avoid gc on coordinate transformation point object */
	point = new point_t;
    }
    void setup() {
	if (window == null)
	    window = {
		width:		MenuDecorationWidth * 2,
		height:		MenuDecorationHeight * 2,
		pixel:		normal_color
	    };
	window.events = (1 << ButtonMotion)	|
			(1 << ButtonRelease)	|
			(1 << Expose)		|
			(1 << Enter)		|
			(1 << Leave);
    }
    void add_item(menuitem_t item) {
	int32_t change;
	int32_t height = item.height;
	if (first == null)
	    first = item.prev = item.next = item;
	else {
	    item.prev = first.prev;
	    first.prev.next = item;
	    first.prev = item;
	    item.next = first;
	}
	item.x = offset;
	offset += item.width;
	item.y = MenuDecorationHeight;
	item.window = window;
	if (window.width < offset + MenuDecorationWidth) {
	    window.width = offset + MenuDecorationWidth;
	    change = true;
	}
	if (window.height < height + MenuDecorationHeight * 2) {
	    window.height = height + MenuDecorationHeight * 2;
	    item = first;
	    do {
		item.height = height;
		item = item.next;
	    } while (item != first);
	    change = true;
	}
	else
	    item.height = window.height - MenuDecorationHeight * 2;
	if (change)
	    change_window(window);
    }
    void paint() {
	menuitem_t item;
	clear_rect(window, 0, 0, window.width, window.height, false);
	paint_relief(window, 0, 0, window.width, window.height, true);
	if (first) {
	    text_gc.drawable = window;
	    change_context(text_gc);
	}
	if (entry) {
	    shadow_gc.drawable = window;
	    change_context(shadow_gc);
	    menu_focus_gc.drawable = window;
	    change_context(menu_focus_gc);
	    select_gc.drawable = window;
	    change_context(select_gc);
	}
	if ((item = first))
	    do {
		item.draw(item == entry);
		item = item.next;
	    } while (item != first);
    }
    void button_press(event_t event) {
	set_focus_widget(this);
	if (entry)
	    popdown();
	else {
	    if (event.detail == Button1 &&
		event.y >= MenuDecorationHeight &&
		event.y < window.height - MenuDecorationHeight)
		select_entry(event.x, true);
	    popup();
	    if (entry) {
		window.events |= (1 << PointerMotion);
		change_window(window);
	    }
	}
    }
    void button_motion(event_t event) {
	if (entry && (event.state & Button1Mask)) {
	    if (event.y >= MenuDecorationHeight &&
		event.y < window.height - MenuDecorationHeight)
		select_entry(event.x, false);
	    else if (entry.popup) {
		trans_coord(window, entry.popup.window, event.x, event.y, point);
		/* event is a "scratch" variable, but not good
		 * "practice" to use it this way..., or the way
		 * to "send" and event to entry.popup */
		event.x = point.x;
		event.y = point.y;
		entry.popup.button_motion(event);
	    }
	}
    }
    void pointer_motion(event_t event) {
	if (entry && event.y >= MenuDecorationHeight &&
	    event.y < window.height - MenuDecorationHeight)
	    select_entry(event.x, false);
    }
    void button_release(event_t event) {
	if (entry && entry.popup) {
	    /* same comments as in button_motion; call popup method
	     * because, at least for now, there is no pointer and/or
	     * keyboard grab */
	    trans_coord(window, entry.popup.window, event.x, event.y, point);
	    event.x = point.x;
	    event.y = point.y;
	    entry.popup.button_release(event);
	}
    }
    void key_press(event_t event, int32_t keysym, string_t buffer);
    void set_entry(menuitem_t item, deselect) {
	if (item) {
	    if (item != entry) {
		clear_rect(window, item.x, item.y,
			   item.width, item.height, true);
		if (entry) {
		    clear_rect(window, entry.x, entry.y,
			       entry.width, entry.height, true);
		    unmap_window(entry.popup.window);
		}
		entry = item;
		popup();
	    }
	}
	else if (deselect)
	    popdown();
    }
    void select_entry(x, deselect) {
	menuitem_t item;
	if ((item = first))
	    do {
		if (x >= item.x && x < item.x + item.width) {
		    set_entry(item, deselect);
		    return;
		}
		item = item.next;
	    } while (item != first);
	set_entry(null, deselect);
    }
    void focus_out() {
	popdown();
	window.events &= ~(1 << PointerMotion);
	change_window(window);
    }
    void popup() {
	if (entry && entry.popup) {
	    trans_coord(window, null, entry.x - 2, window.height - 2, point);
	    entry.popup.window.x = point.x;
	    entry.popup.window.y = point.y;
	    change_window(entry.popup.window);
	    map_window(entry.popup.window);
	    set_grab_widget(this);
	}
    }
    void popdown() {
	if (entry) {
	    clear_rect(window, entry.x, entry.y,
		       entry.width, entry.height, true);
	    if (entry.popup) {
		unmap_window(entry.popup.window);
		set_grab_widget(null);
	    }
	    entry = null;
	}
    }
};

/* FIXME almost a cut&paste of menubar_t, basically a popup version with
 * vertical layout */
class popup_t : widget_t {
    menuitem_t	first;
    menuitem_t	entry;
    int16_t	offset;
    popup_t() {
	offset = MenuDecorationHeight;
	widget_t();
    }
    void setup() {
	widget_t.setup();
	if (window.width < MenuDecorationWidth * 2)
	    window.width = MenuDecorationWidth * 2;
	if (window.height < MenuDecorationHeight * 2)
	    window.height = MenuDecorationHeight * 2;
	window.popup = true;
	window.events = (1 << ButtonMotion)	|
			(1 << PointerMotion)	|
			(1 << ButtonRelease)	|
			(1 << Expose) 	 	|
			(1 << Enter)		|
			(1 << UnmapNotify)	|
			(1 << Leave);
    }
    void add_item(menuitem_t item) {
	int32_t change;
	int32_t width = item.width;
	if (first == null)
	    first = item.prev = item.next = item;
	else {
	    item.prev = first.prev;
	    first.prev.next = item;
	    first.prev = item;
	    item.next = first;
	}
	item.x = MenuDecorationWidth;
	item.y = offset;
	offset += item.height;
	item.window = window;
	if (window.height < offset + MenuDecorationHeight) {
	    window.height = offset + MenuDecorationHeight;
	    change = true;
	}
	if (window.width < width + MenuDecorationWidth * 2) {
	    window.width = width + MenuDecorationWidth * 2;
	    item = first;
	    do {
		item.width = width;
		item = item.next;
	    } while (item != first);
	    change = true;
	}
	else
	    item.width = window.width - MenuDecorationWidth * 2;
	if (change)
	    change_window(window);
    }
    void paint() {
	menuitem_t item;
	clear_rect(window, 0, 0, window.width, window.height, false);
	paint_relief(window, 0, 0, window.width, window.height, true);
	text_gc.drawable = window;
	change_context(text_gc);
	if (entry) {
	    menu_focus_gc.drawable = window;
	    change_context(menu_focus_gc);
	    select_gc.drawable = window;
	    change_context(select_gc);
	}
	if ((item = first))
	    do {
		item.draw(item == entry);
		item = item.next;
	    } while (item != first);
    }
    void button_press(event_t event) {
	if (event.detail == Button1 &&
	    event.y >= MenuDecorationHeight &&
	    event.y < window.height - MenuDecorationHeight)
	    select_entry(event.y, true);
    }
    void button_release(event_t event) {
	if (event.detail == Button1 && entry) {
	    /* simulate a entry callback call */
	    unmap_window(window);
	    set_focus_widget(null);
	    print("entry '%s' selected\n", entry.label);
	}
    }
    void button_motion(event_t event) {
	motion(event);
    }
    void pointer_motion(event_t event) {
	motion(event);
    }
    void set_entry(menuitem_t item, deselect) {
	if (item) {
	    if (item != entry) {
		clear_rect(window, item.x, item.y,
			   item.width, item.height, true);
		if (entry)
		    clear_rect(window, entry.x, entry.y,
			       entry.width, entry.height, true);
		entry = item;
	    }
	}
	else if (deselect)
	    unselect();
    }
    void select_entry(y, deselect) {
	menuitem_t item;
	if ((item = first)) {
	    do {
		if (y >= item.y && y < item.y + item.height) {
		    set_entry(item, deselect);
		    return;
		}
		item = item.next;
	    } while (item != first);
	}
	set_entry(null, deselect);
    }
    void focus_out() {
	unselect();
    }
    void unmap() {
	entry = null;
    }
    void leave(event_t event) {
	unselect();
    }
    void motion(event_t event) {
	if (event.x >= MenuDecorationWidth &&
	    event.y >= MenuDecorationHeight &&
	    event.y < window.height - MenuDecorationHeight &&
	    event.x < window.width - MenuDecorationWidth)
	    select_entry(event.y, true);
	else
	    unselect();
    }
    void unselect() {
	if (entry) {
	    clear_rect(window, entry.x, entry.y,
		       entry.width, entry.height, true);
	    entry = null;
	}
    }
};

void menubar_t.key_press(event_t event, int32_t keysym, string_t buffer) {
    if (entry) {
	popup_t		popup;
	switch (keysym) {
	    case KeyLeft:
		set_entry(entry.prev, false);
		if ((popup = entry.popup))
		    popup.set_entry(popup.first, false);
		break;
	    case KeyRight:
		set_entry(entry.next, false);
		if ((popup = entry.popup))
		    popup.set_entry(popup.first, false);
		break;
	    case KeyUp:
		if ((popup = entry.popup))
		    popup.set_entry(popup.entry ?
				    popup.entry.prev : popup.first.prev, false);
		break;
	    case KeyDown:
		if ((popup = entry.popup))
		    popup.set_entry(popup.entry ?
				    popup.entry.next : popup.first, false);
		break;
	    case KeyHome:
		if ((popup = entry.popup))
		    popup.set_entry(popup.first, false);
		break;
	    case KeyEnd:
		if ((popup = entry.popup))
		    popup.set_entry(popup.first.prev, false);
		break;
	    case KeyEscape:
		popdown();
		break;
	    case KeyReturn:
		if ((popup = entry.popup)) {
		    if (popup.entry) {
			/* simulate a entry callback call */
			print("entry '%s' selected with keyboard\n",
			      popup.entry.label);
			popdown();
		    }
		    else
			popup.set_entry(popup.first, false);
		}
		break;
	}
    }
}

class textfield_t;

/***********************************************************************
 * Selection
 ***********************************************************************/
class selection_t {
    textfield_t	owner;
    textfield_t current() {
	return owner;
    }
    void acquire(textfield_t field);
    void release(textfield_t field) {
	if (owner == field)
	    owner = null;
    }
    string_t fetch() {
	return fetch_bytes();
    }
    void store(string_t buffer, int32_t offset, int32_t length) {
	store_bytes(buffer, offset, length);
    }
};

selection_t global_selection = {
};

void selection_store(string_t buffer, int32_t offset, int32_t length) {
    global_selection.store(buffer, offset, length);
}

string_t selection_fetch() {
    return global_selection.fetch();
}

widget_t selection_owner() {
    return global_selection.owner;
}

void selection_acquire(textfield_t widget) {
    global_selection.acquire(widget);
}

void selection_release(textfield_t widget) {
    global_selection.release(widget);
}

/***********************************************************************
 * Caret
 ***********************************************************************/
/* subclass widget to have simpler timer handling */
class caret_t : widget_t {
    int16_t	x;
    int16_t	y;
    uint16_t	height;
    uint8_t	forced;
    timer_t	time;
    widget_t	owner;
    pixmap_t	image;
    context_t	image_gc;
    context_t	caret_gc;
    caret_t() {
	window = {
	    width:		1,
	    height:		1
	};
	create_window(window);
	rect_t	rect = text_rect(text_gc, "");
	height = rect.height;
	time = {
	    window:		window,
	    msec:		500,
	    repeat:		true
	};
	image = {
	    width:		1,
	    height:		height
	};
	create_pixmap(image);
	image_gc = {
	    drawable:		image,
	    foreground:		0x000000,
	    font:		DefaultFont
	};
	create_context(image_gc);
	caret_gc = {
	    foreground:		0x000000,
	    font:		DefaultFont
	};
	create_context(caret_gc);
    }
    auto get_sensitive() {
	return true;
    }
    void acquire(widget_t widget, int32_t x, int32_t y) {
	if (owner != widget) {
	    this.x = x;
	    this.y = y;
	    owner = widget;
	    caret_gc.drawable = widget.window;
	    change_context(caret_gc);
	    copy();
	    arm_timer();
	}
    }
    void release(widget_t widget) {
	if (owner == widget) {
	    disarm_timer();
	    owner = null;
	}
    }
    void copy() {
	copy_rect(image_gc, 0, 0, owner.window, x, y, 1, height);
    }
    void arm_timer() {
	state = 0;
	create_timer(time);
	dispatch[time] = this;
	/* call callbacks once before triggering timer */
	timer(time);
    }
    void disarm_timer() {
	state = 0;
	destroy_timer(time);
	dispatch[time] = null;
    }
    void hide() {
	copy_rect(caret_gc, x, y, image, 0, 0, 1, height);
    }
    void show() {
	draw_line(caret_gc, x, y, x, y + height - 1);
    }
    void force() {
	/* force caret visible for at least one timer trigger */
	forced = state = true;
	show();
    }
    void timer(timer_t time)
    {
	if (!forced) {
	    if ((state = !state))
		show();
	    else
		hide();
	}
	else
	    forced = false;
    }
};

caret_t global_caret = {
};

void caret_acquire(widget_t widget, int32_t x, int32_t y) {
    global_caret.acquire(widget, x, y);
}

void caret_release(widget_t widget) {
    global_caret.release(widget);
}

widget_t caret_owner() {
    return global_caret.owner;
}

void caret_copy_background() {
    global_caret.copy();
}

auto get_caret_state() {
    return global_caret.state;
}

void set_caret_state(state) {
    global_caret.state = state;
}

void caret_hide() {
    global_caret.hide();
}

void caret_force_visible() {
    global_caret.force();
}

void set_caret_position(x, y) {
    global_caret.x = x;
    global_caret.y = y;
}

class textfield_t : label_t {
    textline_t	textline;	// line where offset is located
    point_t	caret;		// remember about caret coordinates
    point_t	scroll;		// scroll origin coordinate
    uint32_t	offset;		// insert position
    uint32_t	select;		// selection start or end offset
    uint32_t	line_index;	// index of line in line table
    uint32_t	line_count;	// number of visible lines
    textfield_t() {
	int32_t i, size;
	caret = {
	};
	scroll = {
	};
	label_t();
	textline = table[line_index = sizeof(table) - 1];
	offset = select = sizeof(string);
	size = sizeof(string);
	/* FIXME this is bogus, because constant fold and runtime
	 * typeof() implementation masks t_const; either should
	 * not mask t_const, or have a constant_p() primitive;
	 * probably the later is the best approach... */
	if (typeof(string) == typeof("const")) {
	    string_t	buffer = string;
	    string = new uint8_t[size];
	    for (i = size - 1; i >= 0; i--)
		string[i] = buffer[i];
	}
	resize();
	adjust_window();
	tabstop = true;
    }
    void setup() {
	label_t.setup();
	window.pixel = text_back_color;
	window.events = (1 << ButtonMotion) 	|
			(1 << ButtonRelease)	|
			(1 << Expose)		|
			(1 << Enter)		|
			(1 << Leave);
    }
    void focus_in() {
	set_focus_widget(this);
	paint();
	caret_acquire(this, caret.x + scroll.x, caret.y + scroll.y);
    }
    void focus_out() {
	caret_release(this);
	paint();
    }
    void paint() {
	textline_t	temp;
	context_t	gc0, gc1, gc2;
	int32_t		x0, y0, x1, y1;
	int32_t		focus = get_focus_widget() == this;
	int32_t		i, count = sizeof(table);
	int32_t		left, right, sel_left, sel_right;
	int32_t		position = select > offset ? offset : select;
	int32_t		selection = abs(select - offset);
	gc0 = get_sensitive() ? text_gc : sensitive_gc;
	gc0.drawable = window;
	change_context(gc0);
	if (selection) {
	    gc1 = focus ? menu_focus_gc : shadow_gc;
	    gc1.drawable = window;
	    change_context(gc1);
	    gc2 = light_gc;
	    gc2.drawable = window;
	    change_context(gc2);
	    sel_left = position;
	    sel_right = sel_left + selection;
	}
	/* FIXME should only repaint what is actually required */
	/* clear entire window */
	clear_rect(window, 2, 2, window.width - 4, window.height - 4, false);
	for (i = 0; i < count; i++) {
	    temp = table[i];
	    y0 = temp.y + scroll.y;
	    if (y0 <= 2)
		continue;
	    if (y0 + temp.height >= window.height - 2)
		break;
	    left = temp.offset;
	    right = left + temp.length;
	    if (selection) {
		if ((sel_left = position) < left)
		    sel_left = left;
		if ((sel_right = position + selection) > right)
		    sel_right = right;
	    }
	    if (!selection || left >= sel_right || right <= sel_left) {
		x0 = temp.x + scroll.x;
		draw_text(gc0, x0, y0, string, left, temp.length);
		if (selection && position == right) {
		    x0 += temp.width;
		    fill_rect(gc1, x0, y0, rect.height / 3, temp.height + 1);
		}
	    }
	    else {
		x0 = temp.x + scroll.x;
		if (left < sel_left) {
		    text_rect(gc0, string, rect, left, sel_left - left);
		    draw_text(gc0, x0, y0, string, left, sel_left - left);
		    x0 += rect.width;
		}
		text_rect(gc0, string, rect, sel_left, sel_right - sel_left);
		if (right < position + selection)
		    rect.width += rect.height / 3;
		fill_rect(gc1, x0, y0, rect.width, temp.height + 1);
		draw_text(gc2, x0, y0, string,
			  sel_left, sel_right - sel_left);
		if (right > sel_right) {
		    left = sel_right;
		    x0 += rect.width;
		    text_rect(gc0, string, rect, left, right - left);
		    draw_text(gc0, x0, y0, string, left, right - left);
		}
	    }
	}
	if (focus) {
	    gc0 = sensitive_gc;
	    gc1 = menu_focus_gc;
	}
	else {
	    gc0 = shadow_gc;
	    gc1 = normal_gc;
	}
	gc0.drawable = window;
	change_context(gc0);
	gc1.drawable = window;
	change_context(gc1);
	x0 = y0 = 0;
	x1 = window.width;
	y1 = window.height;
	draw_line(gc0,		x1 - 3, y0,	x0 + 1, y0);
	draw_line(gc0,		x0,	y0 + 1, x0,	y1 - 3);
	draw_line(gc1,		x0 + 2,	y1 - 2, x1 - 3, y1 - 2);
	draw_line(gc1,		x1 - 2, y1 - 3, x1 - 2, y0 + 2);
	if (!focus) {
	    gc1 = select_gc;
	    gc1.drawable = window;
	    change_context(gc1);
	}
	draw_line(gc1,		x1 - 3, y0 + 1, x0 + 2, y0 + 1);
	draw_line(gc1,		x0 + 1, y0 + 2, x0 + 1, y1 - 3);
	light_gc.drawable = window;
	change_context(light_gc);
	draw_line(light_gc,	x0 + 2, y1 - 1, x1 - 2, y1 - 1);
	draw_line(light_gc,	x1 - 1, y1 - 3, x1 - 1, y0 + 2);
	text_back_gc.drawable = window;
	change_context(text_back_gc);
	fill_rect(text_back_gc,	x0 + 2, y0 + 2, 3, y1 - 4);
	fill_rect(text_back_gc,	x1 - 5, y0 + 2, 3, y1 - 4);
	if (focus && caret_owner() == this) {
	    caret_copy_background();
	    set_caret_state(false);
	}
    }
    void replace(int32_t left, int32_t right, string_t buffer) {
	textline_t	left_line, right_line;
	int32_t		i, j, k, count, length, size;
	int32_t		left_index, right_index;
	int32_t		offset_change, table_change;
	int32_t		first_newline, last_newline;
	if (left > right)
	    i = left, left = right, right = i;
	count = sizeof(table);
	size = sizeof(string);
	length = sizeof(buffer);
	if ((offset_change = length - (right - left)) < 0) {
	    for (i = right, j = i + offset_change; i < size; i++, j++)
		string[j] = string[i];
	    renew(string, size += offset_change);
	}
	else if (offset_change) {
	    renew(string, size + offset_change);
	    for (i = size - 1, j = i + offset_change; i >= right; i--, j--)
		string[j] = string[i];
	    size = sizeof(string);
	}
	for (i = k = 0, j = left; i < length; i++, j++) {
	    if ((string[j] = buffer[i]) == '\n') {
		if (++k == 1)
		    first_newline = i;
		last_newline = i;
	    }
	}
	left_index = line_index_of_position(left);
	right_index = line_index_of_position(right);
	table_change = k - (right_index - left_index);
	left_line = table[left_index];
	if (left_index == right_index) {
	    if (k == 0)
		left_line.length += offset_change;
	    else
		left_line.length = left - left_line.offset + first_newline;
	}
	else {
	    right_line = table[right_index];
	    if (k == 0)
		left_line.length = left - left_line.offset + length +
		    (right_line.offset + right_line.length - right);
	    else
		left_line.length = left - left_line.offset + first_newline;
	}
	text_rect(text_gc, string, rect, left_line.offset, left_line.length);
	left_line.width = rect.width;
	for (i = right_index + 1; i < count; i++)
	    table[i].offset += offset_change;
	if (table_change) {
	    /* assume fixed height */
	    length = table_change * (rect.height + 1);
	    if (table_change < 0) {
		for (i = right_index + 1, j = i + table_change;
		     i < count; i++, j++) {
		    table[i].y += length;
		    table[j] = table[i];
		}
		renew(table, count += table_change);
	    }
	    else {
		renew(table, count + table_change);
		for (i = count - 1, j = i + table_change;
		     i > right_index; i--, j--) {
		    table[i].y += length;
		    table[j] = table[i];
		}
		count = sizeof(table);
	    }
	}
	right_index = left_index;
	if ((i = first_newline) != last_newline) {
	    right = left_line.y;
	    left = left_line.offset + left_line.length + 1;
	    do {
		j = i;
		for (++i; buffer[i] != '\n'; i++)
		    ;
		length = i - j - 1;
		/* assume fixed height */
		right += rect.height + 1;
		text_rect(text_gc, buffer, rect, j, length);
		/* need to add a new entry as "add_line()" actually
		 * reuses the existing entry, that is now duplicated */
		table[++right_index] = {
		    x:			5,
		    y:			right,
		    width:		rect.width,
		    height:		rect.height,
		    offset:		left,
		    length:		length
		};
		left += length + 1;
	    } while (i != last_newline);
	    for (length = 0, i = left; i < size && string[i] != '\n'; i++)
		++length;
	    text_rect(text_gc, string, rect, left, length);
	    right += rect.height + 1;
	    table[++right_index] = {
		x:			5,
		y:			right,
		width:			rect.width,
		height:			rect.height,
		offset:			left,
		length:			length
	    };
	}
	else if (k) {
	    left += last_newline + 1;
	    /* information was lost when adjusting previous line */
	    for (length = 0, i = left; i < size && string[i] != '\n'; i++)
		++length;
	    text_rect(text_gc, string, rect, left, length);
	    table[++right_index] = {
		x:			5,
		y:			left_line.y + rect.height + 1,
		width:			rect.width,
		height:			rect.height,
		offset:			left,
		length:			length
	    };
	}
    }
    void copy_selection() {
	int32_t	position, length;
	if (select < offset)
	    position = select, length = offset - select;
	else
	    position = offset, length = select - offset;
	selection_store(string, position, length);
    }
    /* TODO should optimize edition, either by using multiple buffers for
     * this.string, or a string that represent the current line */
    void key_press(event_t event, int32_t keysym, string_t buffer) {
	int32_t	text_change, offset_change;
	int32_t	position, selection = select - offset;
	switch (keysym) {
	    case 'c':		case 'C':
		if (event.state & ControlMask)
		    copy_selection();
		else
		    goto insert;
		break;
	    case 'v':		case 'V':
		if (event.state & ControlMask)
		    buffer = selection_fetch();
		goto insert;
	    case 'x':		case 'X':
		if (event.state & ControlMask) {
		    if (selection) {
			copy_selection();
			replace(offset, select, "");
			if (selection < 0)
			    offset = select;
			else
			    select = offset;
			text_change = true;
		    }
		}
		else
		    goto insert;
		break;
	    case KeyDelete:
		if (event.state & ShiftMask)
		    copy_selection();
		if (selection < 0)
		    position = offset, offset = select;
		else if (selection)
		    position = select;
		else
		    position = next_offset(offset);
		if (position != offset) {
		    replace(offset, position, "");
		    select = offset;
		    text_change = true;
		}
		break;
	    case KeyInsert:
		if (event.state & ShiftMask) {
		    buffer = selection_fetch();
		    goto insert;
		}
		break;
	    case KeyBackSpace:
		if (selection < 0)
		    position = offset, offset = select;
		else if (selection)
		    position = select;
		else
		    position = offset, offset = prev_offset(offset);
		if (position != offset) {
		    replace(offset, position, "");
		    select = offset;
		    text_change = true;
		}
		break;
	    case KeyHome:
		if (offset != textline.offset) {
		    offset = textline.offset;
		    offset_change = true;
		}
		break;
	    case KeyLeft:
		if (offset) {
		    offset = prev_offset(offset);
		    offset_change = true;
		}
		break;
	    case KeyUp:
		if (textline.offset) {
		    position = offset;
		    offset = coord_offset(caret.x, caret.y - textline.height);
		    offset_change = offset != position;
		}
		break;
	    case KeyRight:
		if (offset < sizeof(string)) {
		    offset = next_offset(offset);
		    offset_change = true;
		}
		break;
	    case KeyDown:
		if (textline.offset + textline.length < sizeof(string)) {
		    position = offset;
		    offset = coord_offset(caret.x, caret.y + textline.height + 1);
		    offset_change = offset != position;
		}
		break;
	    case KeyPageUp:
		if (line_index) {
		    if ((line_index -= line_count) < 0)
			line_index = 0;
		    textline = table[line_index];
		    offset = coord_offset(caret.x, textline.y);
		    offset_change = true;
		}
		break;
	    case KeyPageDown:
		position = sizeof(table) - 1;
		if (line_index < position) {
		    if ((line_index += line_count) > position)
			line_index = position;
		    textline = table[line_index];
		    offset = coord_offset(caret.x, textline.y);
		    offset_change = true;
		}
		break;
	    case KeyEnd:
		if (offset < textline.offset + textline.length) {
		    offset = textline.offset + textline.length;
		    offset_change = true;
		}
		break;
	    case KeyEscape:
		break;
	    case KeyReturn:
		if (selection < 0)
		    position = offset, offset = select;
		else if (selection)
		    position = select;
		else
		    position = offset;
		replace(offset, position, "\n");
		select = ++offset;
		text_change = true;
		break;
	    default:
	    insert:
		if ((position = sizeof(buffer))) {
		    replace(offset, select, buffer);
		    position += select > offset ? offset : select;
		    select = offset = position;
		    text_change = offset_change = true;
		}
		break;
	}
	if (text_change || offset_change)
	    update(selection, !(event.state & ShiftMask), text_change);
	else if (caret_owner() == this)
	    caret_force_visible();
    }
    void button_press(event_t event) {
	widget_t.button_press(event);
	if (inside && (state & Button1Mask))
	    button_event(event.x - scroll.x, event.y - scroll.y, true);
    }
    void button_motion(event_t event) {
	/* dont check if inside or y coordinate matches, to make it easier
	 * to scroll selection */
	if (state & Button1Mask)
	    button_event(event.x - scroll.x, event.y - scroll.y, false);
    }
    void resize() {
	if ((line_count = (window.height - 10) \ (textline.height + 1)) < 1)
	    line_count = 1;
    }
    widget_t tab_next() {
	if (select != offset) {
	    select = offset;
	    paint();
	}
	return widget_t.tab_next();
    }
    widget_t tab_prev() {
	if (select != offset) {
	    select = offset;
	    paint();
	}
	return widget_t.tab_prev();
    }
    auto next_offset(int32_t position) {
	int32_t size = sizeof(string);
	if (position < size) {
	    for (++position; position < size; ++position)
		if ((string[position] &  0x80) == 0 ||
		    (string[position] & ~0x3f) != 0x80)
		    return position;
	}
	return size;
    }
    auto prev_offset(int32_t position) {
	if (position > 0) {
	    for (--position; position > 0; --position)
		if ((string[position] &  0x80) == 0 ||
		    (string[position] & ~0x3f) != 0x80)
		    return position;
	}
	return 0;
    }
    void button_event(int32_t x, int32_t y, int32_t press) {
	int32_t position = offset;
	int32_t selection = offset - select;
	offset = coord_offset(x, y);
	if (offset != position || (press && selection))
	    update(selection, press, false);
	else if (caret_owner() == this)
	    caret_force_visible();
    }
    auto coord_offset(int32_t x, int32_t y) {
	textline_t	temp;
	int32_t		i, position = offset;
	if (y < textline.y) {
	    for (i = line_index - 1; i >= 0; i--) {
		temp = table[i];
		if (temp.y <= y && temp.y + temp.height >= y) {
		    line_index = i;
		    textline = temp;
		    break;
		}
	    }
	}
	else if (y >= textline.y + textline.height) {
	    int32_t	count = sizeof(table);
	    for (i = line_index + 1; i < count; i++) {
		temp = table[i];
		if (temp.y <= y && temp.y + temp.height >= y) {
		    line_index = i;
		    textline = temp;
		    break;
		}
	    }
	}
	if (x <= textline.x)
	    position = textline.offset;
	else if (x >= textline.x + textline.width)
	    position = textline.offset + textline.length;
	else {
	    int32_t min_pos, max_pos;
	    int32_t width, test_position, test_width;
	    min_pos = textline.offset;
	    max_pos = min_pos + textline.length;
	    /* make line offset calculation easier */
	    x -= textline.x;
	    /* estimate new offset;
	     * should be already correct for non variable width fonts,
	     * and/or if there aren't multibyte characters in string;
	     * use prev_offset() in case calculated offset is in the
	     * middle of an utf8 sequence */
	    position = min_pos + textline.length * x \ textline.width + 1;
	    if (position < min_pos)
		position = min_pos;
	    else if (position > max_pos)
		position = max_pos;
	    if ((position = prev_offset(position)) < min_pos)
		position = min_pos;
	    text_rect(text_gc, string, rect,
		      textline.offset, position - textline.offset);
	    width = rect.width;
	    if (width < x) {
		for (;;) {
		    /* scan to the end of string for better position */
		    if (width > x ||
			(test_position = next_offset(position)) >= max_pos) {
			if (width <= x)
			    position = max_pos;
			break;
		    }
		    test_width = width;
		    text_rect(text_gc, string, rect,
			      position, (test_position - position));
		    width += rect.width;
		    /* choose the closest boundary */
		    if (x - test_width < width - x)
			break;
		    position = test_position;
		}
	    }
	    else if (width > x) {
		for (;;) {
		    /* scan to the start of string for better position */
		    if (width < x ||
			(test_position = prev_offset(position)) <= min_pos) {
			if (width >= x)
			    position = min_pos;
			break;
		    }
		    test_width = width;
		    text_rect(text_gc, string, rect,
			      test_position, (position - test_position));
		    width -= rect.width;
		    /* choose the closest boundary */
		    if (test_width - x < x - width)
			break;
		    position = test_position;
		}
	    }
	}
	return position;
    }
    auto line_index_of_position(int32_t position) {
	textline_t	line;
	int32_t		i, count;
	if (position < textline.offset) {
	    for (i = line_index - 1; i >= 0; i--)
		if (table[i].offset <= position)
		    return i;
	}
	else if (position > textline.offset + textline.length) {
	    for (i = line_index + 1, count = sizeof(table); i < count; i++) {
		line = table[i];
		if (line.offset <= position &&
		    line.offset + line.length >= position)
		    return i;
	    }
	}
	return line_index;
    }
    void adjust_window() {
	int32_t	index;
	/* textline/line_index may be out of sync if pasting or deleting
	 * multiple lines */
	if (line_index >= sizeof(table))
	    textline = table[line_index = sizeof(table) - 1];
	do {
	    index = line_index_of_position(offset);
	    if (line_index != index)
		textline = table[line_index = index];
	} while (offset < textline.offset ||
		 offset > textline.offset + textline.length);
	if (textline.y + scroll.y < 0)
	    scroll.y = -textline.y + 5;
	else if (textline.y + textline.height + scroll.y > window.height - 10) {
	    if ((index = line_index - line_count + 1) < 0)
		index = 0;
	    scroll.y = -table[index].y + 5;
	}
	text_rect(text_gc, string, rect,
		  textline.offset, offset - textline.offset);
	caret.y = textline.y;
	caret.x = textline.x + rect.width;
	if (scroll.x + caret.x < 5)
	    scroll.x = -caret.x + 5;
	else if (scroll.x + caret.x > window.width - 12)
	    scroll.x = window.width - 12 - caret.x + 5;
	if (caret_owner() == this)
	    set_caret_position(caret.x + scroll.x, caret.y + scroll.y);
    }
    void update(selection, reset_selection, text_change) {
	int32_t repaint;
	int32_t sx = scroll.x, sy = scroll.y;
	int32_t have_caret = caret_owner() == this;
	if (have_caret && get_caret_state())
	    caret_hide();
	if (reset_selection)
	    select = offset;
	repaint = text_change || selection != select - offset;
	adjust_window();
	if (!repaint)
	    repaint = sx != scroll.x || sy != scroll.y;
	if (repaint)
	    paint();
	else if (have_caret)
	    caret_copy_background();
	if (select != offset) {
	    if (selection_owner() != this)
		selection_acquire(this);
	}
	else if (selection_owner() == this)
	    selection_release(this);
	if (have_caret)
	    caret_force_visible();
    }
    void loose_selection() {
	if (select != offset) {
	    select = offset;
	    paint();
	}
    }
};

void selection_t.acquire(textfield_t field) {
    if (owner != field) {
	if (owner)
	    owner.loose_selection();
	owner = field;
    }
}

class box_t : composite_t {
    uint16_t	offset;
    uint16_t	distance;
    uint8_t	orientation;
    box_t() {
	offset = distance;
	composite_t();
    }
    void manage(widget_t widget) {
	if (orientation == Vertical) {
	    widget.window.x = distance;
	    widget.window.y = offset;
	    offset += widget.window.height + distance;
	    if (window.width < widget.window.width + distance)
		window.width = widget.window.width + distance;
	    window.height = offset;
	}
	else {
	    widget.window.x = offset;
	    widget.window.y = distance;
	    offset += widget.window.width + distance;
	    window.width = offset;
	    if (window.height < widget.window.height + distance)
		window.height = widget.window.height + distance;
	}
	composite_t.manage(widget);
    }
};

toplevel_t top = {
    window: {
	width:		200,
	height:		200,
	pixel:		normal_color,
	input:		true
    }
};

menubar_t bar = {
    parent:		top,
};

popup_t file_popup = {
    parent:		bar,
};
file_popup.add_item(new menuitem_t { label: "Open" });
file_popup.add_item(new menuitem_t { label: "Close" });
file_popup.add_item(new menuitem_t { label: "Quit" });
menuitem_t file_item = {
    label:		"File",
    popup:		file_popup
};
bar.add_item(file_item);

popup_t help_popup = {
    parent:		bar,
};
help_popup.add_item(new menuitem_t { label: "About" });
menuitem_t help_item = {
    label:		"Help",
    popup:		help_popup
};
bar.add_item(help_item);

viewport_t view = {
    parent:		top
};
view.clipview = new clipwidget_t {
    parent:		view,
};
view.hscroll = {
    parent:		view,
    orientation:	Horizontal,
    clip:		view.clipview
};
view.vscroll = {
    parent:		view,
    orientation:	Vertical,
    clip:		view.clipview
};

clipwidget_t clip = view.clipview;

box_t box = {
    parent:		clip.widget,
    distance:		10,
    orientation:	Vertical,
};

button_t ok = {
    parent:		box,
    string:		"Ok",
    window: {
	width:		80,
	height:		26,
	pixel:		normal_color,
    }
};
button_t cancel = {
    parent:		box,
    string:		"Cancel\nMaybe\nDon't know, really",
    window: {
	//width:		80,
	//height:		26,
	pixel:		normal_color,
    }
};
textfield_t text = {
    parent:		box,
    string:		"text\nwith\nmultiple\nnewlines",
    window: {
	width:		120,
	height:		58,
	pixel:		text_back_color,
    }
};

top.realize();
top.event_loop();

close_display();
